//
//  StudyStateMachine.swift
//  SitPosition
//
//  Created by apple on 2019/2/17.
//  Copyright © 2019 apple. All rights reserved.
//

import Foundation

// 图片后缀名
let photoExtension: String = "jpg"

/**
 * 学习姿态状态机
 * 从外部接收姿态信息，并完成如下任务：
 * 保存当前姿态和前一个姿态的信息，姿态信息包含：类型、开始时间、最后更新时间
 */
class StudyStateMachine {
    
    // 触发报警通知
    // userInfo："poseType" -> poseType
    public static let triggerAlarmNotification = Notification.Name("triggerAlarmNotification")
    public static let triggerAlarmNoteKeyPoseType = "poseType"
    
    // 状态机中的某一时间点上的状态快照
    // 这里继承自NSObject，借用了NSObject的Equalable默认实现
    fileprivate class Item : NSObject {
        let poseType: PoseType
        let snapshotFile: String
        var createTI: TimeInterval
        var lastTI: TimeInterval
        
        // 持续时长
        var duration: TimeInterval {
            return lastTI - createTI
        }
        
        init(_ poseType: PoseType, snapshotFile: String, createTI: TimeInterval) {
            self.poseType = poseType
            self.snapshotFile = snapshotFile
            self.createTI = createTI
            self.lastTI = createTI
        }
        
        // 获取缓存文件全路径（内部使用）
        static func snapshotCachePath(_ filename: String) -> String? {
            guard let dirPath: String = try? Paths.snapshotCachePath() else {
                return nil
            }
            return (dirPath as NSString).appendingPathComponent(filename)
        }
    }
    
    // 获取缓存文件全路径（外部使用）
    public static func snapshotCachePath(_ filename: String) -> String? {
        return Item.snapshotCachePath(filename)
    }
    
    // 全局单例
    public static let instance: StudyStateMachine = StudyStateMachine()
    
    // 工作线程
    private var canExitWorkerThread: Bool = false
    private let workerThreadSemaphore: DispatchSemaphore = DispatchSemaphore(value: 0)
    private lazy var workerThread: Thread = {
        return Thread(target: self, selector: #selector(_workerThread(_:)), object: nil)
    }()
    
    // 是否启动标志？
    private var isStarted: Bool = false
    // 定时器（定时上传用户姿态）
    private var periodicTimer: Timer? = nil
    
    // 状态快照原始（输入）队列
    fileprivate var origItems: [Item] = []
    fileprivate let origItemsLock: NSLock = NSLock()
    
    // 报警数据（输出）队列
    fileprivate var resultItems: [Item] = []
    fileprivate let resultItemsLock: NSLock = NSLock()
    
    // MARK: - Object lifecycle
    
    deinit {
        periodicTimer?.invalidate()
        periodicTimer = nil
        canExitWorkerThread = true
        workerThreadSemaphore.signal()
    }
    
    // MARK: - Public Methods
    
    // 入队一个姿态
    @discardableResult
    public func queue(_ poseType: PoseType, image: UIImage) -> Bool {
        origItemsLock.lock()
        defer { origItemsLock.unlock() }
        
        /// 队列为空：数值入队 》返回
        guard let head = origItems.first else {
            return _queueOnHead(poseType, image: image)
        }
        
        /// 队列非空
        
        // 与队首数值比较，相同则与队首项合并为一项，并走持续时间阈值触发检查流程；
        if head.poseType == poseType {
            head.lastTI = Date().timeIntervalSince1970
            _checkContinuousTimeThresholdCondition(head)
            return true
        }
        
        // 不同则走如下流程
        if origItems.count == 1 {
            // 队列有一项数值：数值入队 》返回。
            return _queueOnHead(poseType, image: image)
        } else if origItems.count == 2 {
            // 队列有二项数值：检查新数值是否与队尾数值类型相同，
            // 相同：删除队首数值并与队尾数值合并为一项，并走持续时间阈值触发检查流程；
            // 不同：删除队尾数值并压入新数值。
            guard let tail = origItems.last else {
                fatalError("Internal data structure is corrupted")
            }
            if tail.poseType == poseType {
                _deleteHead()
                tail.lastTI = Date().timeIntervalSince1970
                _checkContinuousTimeThresholdCondition(tail)
            } else {
                _deleteTail()
                return _queueOnHead(poseType, image: image)
            }
        } else {
            // 始终保持预处理队列只有两项姿态数据项
            origItems = Array<StudyStateMachine.Item>(origItems.dropLast(origItems.count - 1))
            return _queueOnHead(poseType, image: image)
        }
        return false
    }
    
}


// MARK: - Private Methods

extension StudyStateMachine {
    
    // 持续时间报警阀值
    private var continousTimeThreshold: Double {
        // 获取学习姿态报警时间阀值设置
        let config: UserConfigs? = UserConfigs.current
        let threshold: Double
        #if DEBUG
        threshold = 3
        #else
        threshold = Double(config?.alarmTime ?? UserConfigs.defaultAlarmTime)
        #endif
        return threshold
    }
    
    // 在队首插入一个姿态
    @discardableResult
    private func _queueOnHead(_ poseType: PoseType, image: UIImage) -> Bool {
        guard let filename: String = saevSnapshot(image) else {
            return false
        }
        let newItem: Item = Item(poseType, snapshotFile: filename, createTI: Date().timeIntervalSince1970)
        origItems.insert(newItem, at: 0)
        _startIfNeeded()
        return true
    }
    
    // 删除队首姿态数据
    @discardableResult
    private func _deleteHead() -> Bool {
        guard let head = origItems.first else { return false }
        
        // 清理对应的缓存文件
        if let path = Item.snapshotCachePath(head.snapshotFile) {
            try? FileManager.default.removeItem(atPath: path)
        }
        origItems.removeFirst()
        return true
    }
    
    // 删除队尾姿态数据
    @discardableResult
    private func _deleteTail() -> Bool {
        guard let tail = origItems.last else { return false }
        
        // 清理对应的缓存文件
        if let path = Item.snapshotCachePath(tail.snapshotFile) {
            try? FileManager.default.removeItem(atPath: path)
        }
        origItems.removeLast()
        return true
    }
    
    // 缓存快照用于后续批量上传
    // @return - 返回本地缓存的文件名，nil表示失败。
    private func saevSnapshot(_ image: UIImage) -> String? {
        let filename: String = "\(UUID().uuidString).\(photoExtension)"
        guard let path: String = Item.snapshotCachePath(filename) else {
            return nil
        }
        guard let imageData: Data = image.jpegData(compressionQuality: 0.9) else {
            return nil
        }
        let url: URL = URL(fileURLWithPath: path)
        do {
            try imageData.write(to: url)
            return filename
        } catch {
            return nil
        }
    }
    
    // 启动工作线程（没有启动的前提下）
    private func _startIfNeeded() {
        defer { workerThreadSemaphore.signal() }
        guard !isStarted else { return }
        isStarted = true
        workerThread.start()
        _startPeriodicTimer()
    }
    
    // 启动定时器，定时上传用户姿态数据
    private func _startPeriodicTimer() {
        guard periodicTimer == nil else { return }
        let timer = Timer(timeInterval: 10, repeats: true) { [weak self] (timer) in
            guard let sself = self else { return }
            
            DispatchQueue.global(qos: DispatchQoS.QoSClass.background).async {
                // 每间隔10s上传一次用户姿态数据
                sself.origItemsLock.lock()
                defer {
                    sself.origItemsLock.unlock()
                }
                if let head = sself.origItems.first  {
                    sself.resultItemsLock.lock()
                    sself.resultItems.append(head)
                    sself.resultItemsLock.unlock()
                    
                    sself.workerThreadSemaphore.signal()
                }
            }
        }
        periodicTimer = timer
        RunLoop.main.add(timer, forMode: RunLoop.Mode.common)
    }
    
    // 持续时间阈值触发检查流程
    // 达到触发报警条件就丢到报警队列并通知工作线程进行处理
    private func _checkContinuousTimeThresholdCondition(_ item: StudyStateMachine.Item) {
        guard item.duration >= continousTimeThreshold else {
            return
        }
        resultItemsLock.lock()
        resultItems.append(item)
        resultItemsLock.unlock()
        origItems.removeAll { $0 == item }
        workerThreadSemaphore.signal()
    }
    
}

// MARK: - Worker Thread

extension StudyStateMachine {
    
    // 工作队列线程
    @objc private func _workerThread(_ object: Any?) {
        let jsonEncoder: JSONEncoder = JSONEncoder()
        repeat {
            workerThreadSemaphore.wait()
            
            resultItemsLock.lock()
            
            // 批量上报
            let readyForUploadItems = resultItems
            for item in readyForUploadItems {
                // 确保用户已登录
                guard UserManager.manager.isLogin(),
                    let userId = UserManager.manager.user?.id else {
                    continue
                }
                // 确保快照缓存存在
                guard let path: String = Item.snapshotCachePath(item.snapshotFile) else {
                    continue
                }
                defer { try? FileManager.default.removeItem(atPath: path) }
                guard let data: Data = try? Data(contentsOf: URL(fileURLWithPath: path)) else {
                    continue
                }
                // 构造附加JSON串，作为上传任务的上下文，上传成功后
                // 由StudyDataPushManager组件负责读出该附加信息并提交给后台服务器
                let poseData: StudyPoseData = StudyPoseData(url: "",
                                                            type: item.poseType.networkValue,
                                                            uploadTime: Int64(item.createTI*1000),
                                                            duration: Int64(item.duration*1000))
                
                let key: String = (item.snapshotFile as NSString).deletingPathExtension
                guard let extraData: Data = try? jsonEncoder.encode(poseData),
                    let extra: String = String(data: extraData, encoding: String.Encoding.utf8) else {
                    continue
                }
                try? UploadManager.manager.putData(data: data, key: key, userId: userId, extra: extra)
                
                // 发送报警通知
                let userInfo: [AnyHashable : Any] = [ StudyStateMachine.triggerAlarmNoteKeyPoseType : item.poseType ]
                NotificationCenter.default.post(name: StudyStateMachine.triggerAlarmNotification,
                                                object: nil, userInfo: userInfo)
            }
            resultItems.removeAll { readyForUploadItems.contains($0) }
            resultItemsLock.unlock()
            
            Thread.sleep(forTimeInterval: 1)
        } while (!canExitWorkerThread)
    }
}
